AI챗봇_04_1GB 서버 배포 생존기
계획이 부족했던 자의 최후
처음에 1GB 서버에 스프링을 띄우겠다는 야심찬 계획을 세웠었다. 메모리가 부족하면 Swap을 쓰면 되고, 가볍게 만들면 된다고 생각했다.
out: #11 504.9 BUILD SUCCESSFUL in 8m 23s
단순 빌드에 8분이 걸렸다. 그동안 오라클 서버의 CPU는 100%를 치고, 메모리는 바닥나서 디스크(Swap)를 미친 듯이 긁어댔다. Thrashing이었다.
메모리 돌려막기
처음 든 생각은 메모리가 없으면 만들어내자는 것이었다.
백엔드 빌드할 때 잠시 프론트엔드를 끄면 200MB 정도 확보되겠다는 계산이 섰다.
그래서 CD 파이프라인을 수정했다. 프론트엔드 배포를 먼저 끝내고, 프론트엔드를 강제로 끄고, 백엔드를 빌드한 다음 다시 프론트엔드를 켜는 식이었다.
결과는 성공적이었다. 빌드 시간 동안 사이트가 죽는다는 치명적인 단점이 있었지만, 서버가 OOM으로 뻗지는 않았다.
Docker 레이어 캐싱 적용
다음 문제는 속도였다. 모든 라이브러리를 매번 새로 받는 문제를 없애고자 Dockerfile을 레이어 캐싱(Layer Caching) 구조로 뜯어고쳤다.
의존성 파일만 먼저 복사하고, 의존성을 미리 다운로드하게 했더니 효과는 확실했다. 하지만 여전히 소스 코드를 컴파일(CompileKotlin)하는 시간은 줄일 수 없었다. 1GB 램에서 JVM을 돌리는 것 자체가 고역이었다.
서버에서 빌드하지 않으면 된다
문득 프론트엔드 배포 로그를 보다가 바보같다는 생각이 들었다. 프론트엔드는 이미 GitHub Actions에서 빌드하고 결과물만 서버로 보내고 있었다. 그런데 백엔드는 왜 소스 코드를 서버로 보내서 그 약한 서버가 낑낑대며 빌드하게 만들었을까.
성능 좋은 GitHub Actions 머신에서 빌드해서 JAR 파일만 넘기면 되는 거였다.
GitHub Actions 워크플로우를 전면 수정했다. ubuntu-latest 환경에서 Gradle로 빠르게 빌드하고, 만들어진 파일을 서버에 전송한다.
서버의 Dockerfile은 이제 빌드 과정 없이 단순히 JAR 파일을 실행하는 역할만 한다.
이제 배포는 2분 안에 끝난다. 서버 CPU 점유율은 얌전하다. 사이트 중단도 거의 없다.
삽질하면서 Docker Caching과 Swap 메모리 관리는 확실히 배웠지만, 아무리 생각해도 왜 떠올리지 못한건지.... 바보같긴 하다.